/* Copyright (c) 2010, Carl Burch. License information is located in the
 * com.cburch.logisim.Main source code and at www.cburch.com/logisim/. */

package com.cburch.logisim.std.io;

import java.awt.Color;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.event.KeyEvent;
import java.util.ArrayList;

import com.cburch.logisim.data.Attribute;
import com.cburch.logisim.data.AttributeOption;
import com.cburch.logisim.data.Attributes;
import com.cburch.logisim.data.BitWidth;
import com.cburch.logisim.data.Bounds;
import com.cburch.logisim.data.Direction;
import com.cburch.logisim.data.Value;
import com.cburch.logisim.instance.Instance;
import com.cburch.logisim.instance.InstanceFactory;
import com.cburch.logisim.instance.InstancePainter;
import com.cburch.logisim.instance.InstancePoker;
import com.cburch.logisim.instance.InstanceState;
import com.cburch.logisim.instance.Port;
import com.cburch.logisim.instance.StdAttr;

public class Keyboard extends InstanceFactory {
	public static class Poker extends InstancePoker {
		public void draw(InstancePainter painter) {
			KeyboardData data = getKeyboardState(painter);
			Bounds bds = painter.getInstance().getBounds();
			Graphics g = painter.getGraphics();
			FontMetrics fm = g.getFontMetrics(DEFAULT_FONT);

			String str;
			int cursor;
			int dispStart;
			synchronized (data) {
				str = data.toString();
				cursor = data.getCursorPosition();
				if (!data.isDisplayValid())
					data.updateDisplay(fm);
				dispStart = data.getDisplayStart();
			}

			int asc = fm.getAscent();
			int x = bds.getX() + 8;
			if (dispStart > 0) {
				x += fm.stringWidth(str.charAt(0) + "m");
				x += fm.stringWidth(str.substring(dispStart, cursor));
			} else if (cursor >= str.length()) {
				x += fm.stringWidth(str);
			} else {
				x += fm.stringWidth(str.substring(0, cursor));
			}
			int y = bds.getY() + (bds.getHeight() + asc) / 2;
			g.drawLine(x, y - asc, x, y);
		}

		@Override
		public void keyPressed(InstanceState state, KeyEvent e) {
			KeyboardData data = getKeyboardState(state);
			boolean changed = false;
			boolean used = true;
			synchronized (data) {
				switch (e.getKeyCode()) {
				case KeyEvent.VK_DELETE:
					changed = data.delete();
					break;
				case KeyEvent.VK_LEFT:
					data.moveCursorBy(-1);
					break;
				case KeyEvent.VK_RIGHT:
					data.moveCursorBy(1);
					break;
				case KeyEvent.VK_HOME:
					data.setCursor(0);
					break;
				case KeyEvent.VK_END:
					data.setCursor(Integer.MAX_VALUE);
					break;
				default:
					used = false;
				}
			}
			if (used)
				e.consume();
			if (changed)
				state.getInstance().fireInvalidated();
		}

		@Override
		public void keyTyped(InstanceState state, KeyEvent e) {
			KeyboardData data = getKeyboardState(state);
			char ch = e.getKeyChar();
			boolean changed = false;
			if (ch != KeyEvent.CHAR_UNDEFINED) {
				if (!Character.isISOControl(ch) || ch == '\b' || ch == '\n' || ch == FORM_FEED) {
					synchronized (data) {
						changed = data.insert(ch);
					}
					e.consume();
				}
			}
			if (changed)
				state.getInstance().fireInvalidated();
		}
	}

	private static final int CLR = 0;
	private static final int CK = 1;
	private static final int RE = 2;
	private static final int AVL = 3;

	private static final int OUT = 4;
	private static final int DELAY0 = 9;

	private static final int DELAY1 = 11;
	static final int WIDTH = 145;

	static final int HEIGHT = 25;
	private static final Font DEFAULT_FONT = new Font("sans serif", Font.PLAIN, 12);

	private static final char FORM_FEED = '\u000c'; // control-L

	public static final AttributeOption B7 = new AttributeOption(7, Strings.getter("ASCII (7-bit)"));
	public static final AttributeOption B16 = new AttributeOption(16, Strings.getter("UTF-16 (16-bit)"));
	private static final Attribute<AttributeOption> CHAR_BITS = Attributes.forOption("Char bit width",
			Strings.getter("stdDataWidthAttr"), new AttributeOption[] { B7, B16 });

	private static final Attribute<Integer> ATTR_BUFFER = Attributes.forIntegerRange("buflen",
			Strings.getter("keybBufferLengthAttr"), 1, 256);

	public static void addToBuffer(InstanceState state, char[] newChars) {
		KeyboardData keyboardData = getKeyboardState(state);
		for (int i = 0; i < newChars.length; i++) {
			keyboardData.insert(newChars[i]);
		}
	}

	private static int getBufferLength(Object bufferAttr) {
		if (bufferAttr instanceof Integer)
			return ((Integer) bufferAttr).intValue();
		else
			return 32;
	}

	private static KeyboardData getKeyboardState(InstanceState state) {
		int bufLen = getBufferLength(state.getAttributeValue(ATTR_BUFFER));
		KeyboardData ret = (KeyboardData) state.getData();
		if (ret == null) {
			ret = new KeyboardData(bufLen);
			state.setData(ret);
		} else {
			ret.updateBufferLength(bufLen);
		}
		return ret;
	}

	public Keyboard() {
		super("Keyboard", Strings.getter("keyboardComponent"));
		setAttributes(new Attribute[] { CHAR_BITS, ATTR_BUFFER, StdAttr.EDGE_TRIGGER, },
				new Object[] { B7, Integer.valueOf(32), StdAttr.TRIG_RISING, });
		setOffsetBounds(Bounds.create(0, -15, WIDTH, HEIGHT));
		setIconName("keyboard.gif");
		setInstancePoker(Poker.class);
	}

	@Override
	protected void configureNewInstance(Instance instance) {
		instance.addAttributeListener();
		updateports(instance);
	}

	private void drawBuffer(Graphics g, FontMetrics fm, String str, int dispStart, int dispEnd,
			ArrayList<Integer> specials, Bounds bds) {
		int x = bds.getX();
		int y = bds.getY();

		g.setFont(DEFAULT_FONT);
		if (fm == null)
			fm = g.getFontMetrics();
		int asc = fm.getAscent();
		int x0 = x + 8;
		int ys = y + (HEIGHT + asc) / 2;
		int dotsWidth = fm.stringWidth("m");
		int xs;
		if (dispStart > 0) {
			g.drawString(str.substring(0, 1), x0, ys);
			xs = x0 + fm.stringWidth(str.charAt(0) + "m");
			drawDots(g, xs - dotsWidth, ys, dotsWidth, asc);
			String sub = str.substring(dispStart, dispEnd);
			g.drawString(sub, xs, ys);
			if (dispEnd < str.length()) {
				drawDots(g, xs + fm.stringWidth(sub), ys, dotsWidth, asc);
			}
		} else if (dispEnd < str.length()) {
			String sub = str.substring(dispStart, dispEnd);
			xs = x0;
			g.drawString(sub, xs, ys);
			drawDots(g, xs + fm.stringWidth(sub), ys, dotsWidth, asc);
		} else {
			xs = x0;
			g.drawString(str, xs, ys);
		}

		if (specials.size() > 0) {
			drawSpecials(specials, x0, xs, ys, asc, g, fm, str, dispStart, dispEnd);
		}
	}

	private void drawDots(Graphics g, int x, int y, int width, int ascent) {
		int r = width / 10;
		if (r < 1)
			r = 1;
		int d = 2 * r;
		if (2 * r + 1 * d <= width)
			g.fillOval(x + r, y - d, d, d);
		if (3 * r + 2 * d <= width)
			g.fillOval(x + 2 * r + d, y - d, d, d);
		if (5 * r + 3 * d <= width)
			g.fillOval(x + 3 * r + 2 * d, y - d, d, d);
	}

	private void drawSpecials(ArrayList<Integer> specials, int x0, int xs, int ys, int asc, Graphics g, FontMetrics fm,
			String str, int dispStart, int dispEnd) {
		int[] px = new int[3];
		int[] py = new int[3];
		for (Integer special : specials) {
			int code = special.intValue();
			int pos = code & 0xFF;
			int w0;
			int w1;
			if (pos == 0) {
				w0 = x0;
				w1 = x0 + fm.stringWidth(str.substring(0, 1));
			} else if (pos >= dispStart && pos < dispEnd) {
				w0 = xs + fm.stringWidth(str.substring(dispStart, pos));
				w1 = xs + fm.stringWidth(str.substring(dispStart, pos + 1));
			} else {
				continue; // this character is not in current view
			}
			w0++;
			w1--;

			int key = code >> 16;
			if (key == '\b') {
				int y1 = ys - asc / 2;
				g.drawLine(w0, y1, w1, y1);
				px[0] = w0 + 3;
				py[0] = y1 - 3;
				px[1] = w0;
				py[1] = y1;
				px[2] = w0 + 3;
				py[2] = y1 + 3;
				g.drawPolyline(px, py, 3);
			} else if (key == '\n') {
				int y1 = ys - 3;
				px[0] = w1;
				py[0] = ys - asc;
				px[1] = w1;
				py[1] = y1;
				px[2] = w0;
				py[2] = y1;
				g.drawPolyline(px, py, 3);
				px[0] = w0 + 3;
				py[0] = y1 - 3;
				px[1] = w0;
				py[1] = y1;
				px[2] = w0 + 3;
				py[2] = y1 + 3;
				g.drawPolyline(px, py, 3);
			} else if (key == FORM_FEED) {
				g.drawRect(w0, ys - asc, w1 - w0, asc);
			}
		}
	}

	@Override
	protected void instanceAttributeChanged(Instance instance, Attribute<?> attr) {
		if (attr == CHAR_BITS)
			updateports(instance);
	}

	@Override
	public void paintInstance(InstancePainter painter) {
		boolean showState = painter.getShowState();
		Graphics g = painter.getGraphics();
		painter.drawRoundBounds(Color.WHITE);
		painter.drawClock(CK, Direction.EAST);
		painter.drawPort(CLR);
		painter.drawPort(RE);
		painter.drawPort(AVL);
		painter.drawPort(OUT);

		if (showState) {
			String str;
			int dispStart;
			int dispEnd;
			ArrayList<Integer> specials = new ArrayList<Integer>();
			FontMetrics fm = null;
			KeyboardData state = getKeyboardState(painter);
			synchronized (state) {
				str = state.toString();
				for (int i = state.getNextSpecial(0); i >= 0; i = state.getNextSpecial(i + 1)) {
					char c = state.getChar(i);
					specials.add(Integer.valueOf(c << 16 | i));
				}
				if (!state.isDisplayValid()) {
					fm = g.getFontMetrics(DEFAULT_FONT);
					state.updateDisplay(fm);
				}
				dispStart = state.getDisplayStart();
				dispEnd = state.getDisplayEnd();
			}

			if (str.length() > 0) {
				Bounds bds = painter.getBounds();
				drawBuffer(g, fm, str, dispStart, dispEnd, specials, bds);
			}
		} else {
			Bounds bds = painter.getBounds();
			int len = getBufferLength(painter.getAttributeValue(ATTR_BUFFER));
			String str = Strings.get("keybDesc", "" + len);
			FontMetrics fm = g.getFontMetrics();
			int x = bds.getX() + (WIDTH - fm.stringWidth(str)) / 2;
			int y = bds.getY() + (HEIGHT + fm.getAscent()) / 2;
			g.drawString(str, x, y);
		}
	}

	@Override
	public void propagate(InstanceState circState) {
		Object trigger = circState.getAttributeValue(StdAttr.EDGE_TRIGGER);
		KeyboardData state = getKeyboardState(circState);
		Value clear = circState.getPort(CLR);
		Value clock = circState.getPort(CK);
		Value enable = circState.getPort(RE);
		char c;

		synchronized (state) {
			Value lastClock = state.setLastClock(clock);
			if (clear == Value.TRUE) {
				state.clear();
			} else if (enable != Value.FALSE) {
				boolean go;
				if (trigger == StdAttr.TRIG_FALLING) {
					go = lastClock == Value.TRUE && clock == Value.FALSE;
				} else {
					go = lastClock == Value.FALSE && clock == Value.TRUE;
				}
				if (go)
					state.dequeue();
			}

			c = state.getChar(0);
		}
		boolean ascii = circState.getAttributeValue(CHAR_BITS) == B7;
		Value out = Value.createKnown(BitWidth.create(ascii ? 7 : 16), ascii ? c & 0x7F : c);
		circState.setPort(OUT, out, DELAY0);
		circState.setPort(AVL, c != '\0' ? Value.TRUE : Value.FALSE, DELAY1);
	}

	private void updateports(Instance instance) {
		int b = instance.getAttributeValue(CHAR_BITS) == B7 ? 7 : 16;
		String s = b == 7 ? "ASCII" : "UTF-16";
		Port[] ps = new Port[5];
		ps[CLR] = new Port(20, 10, Port.INPUT, 1);
		ps[CK] = new Port(0, 0, Port.INPUT, 1);
		ps[RE] = new Port(10, 10, Port.INPUT, 1);
		ps[AVL] = new Port(130, 10, Port.OUTPUT, 1);
		ps[OUT] = new Port(140, 10, Port.OUTPUT, b);
		ps[CLR].setToolTip(Strings.getter("keybClearTip"));
		ps[CK].setToolTip(Strings.getter("keybClockTip"));
		ps[RE].setToolTip(Strings.getter("keybEnableTip"));
		ps[AVL].setToolTip(Strings.getter("keybAvailTip"));
		ps[OUT].setToolTip(Strings.getter("keybOutputTip", s));
		instance.setPorts(ps);
	}
}
